﻿namespace NJQuery {
	using System;
	using System.Web.UI;
	using System.Web.UI.WebControls;
	using System.ComponentModel;
	using System.Collections.Specialized;
	using System.Globalization;
	using System.Diagnostics.CodeAnalysis;

	/// <summary>
	/// Server control used to generate a <see href="http://jqueryui.com/demos/tabs/">jQuery UI Tabs widget</see>.
	/// </summary>
	[ParseChildren(true), PersistChildren(false)]
	public class Tabs : ManagedWebControl, IPostBackDataHandler {


		#region Constants: Initialization option defaults

		private const bool defaultCache = false;
		private const bool defaultCollapsible = false;
		private const PanelActivateEvent defaultEvent = PanelActivateEvent.Click;
		private const string defaultIdPrefix = "ui-tabs-";
		private const string defaultPanelTemplate = "<div></div>";
		private const int defaultSelected = 0;
		private const string defaultSpinner = "<em>Loading&#8230;</em>";
		private const string defaultTabTemplate = "<li><a href=\"#{href}\"><span>#{label}</span></a></li>";

		#endregion


		#region Fields

		private TabPanelCollection panelCollection;
		private ITemplate panelTemplate;
		private GroupControlContainer panelTemplateContainer;
		private ITemplate spinner;
		private GroupControlContainer spinnerContainer;
		private ITemplate tabTemplate;
		private GroupControlContainer tabTemplateContainer;
		private TabPanel pendingSelect;
		private bool selectPending;
		private bool selectPendingByPostBack;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="Tabs"/> class.
		/// </summary>
		public Tabs() : base(HtmlTextWriterTag.Div) { }

		#endregion


		#region Properties: Widget initialization options

		/// <summary>
		/// Whether or not to cache remote tabs content, e.g. load only once or with every click. Cached content is being lazy loaded, e.g once and only once for the first click. Note that to prevent the actual Ajax requests from being cached by the browser you need to provide an extra cache: false flag to ajaxOptions.
		/// This property maps directly to the "cache" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultCache), Category("Behavior")]
		public virtual bool Cache {
			get { return ViewState["Cache"] as bool? ?? defaultCache; }
			set { ViewState["Cache"] = value; }
		}

		/// <summary>
		/// Set to true to allow an already selected tab to become unselected again upon reselection.
		/// This property maps directly to the "collapsible" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultCollapsible), Category("Behavior")]
		public virtual bool Collapsible {
			get { return ViewState["Collapsible"] as bool? ?? defaultCollapsible; }
			set { ViewState["Collapsible"] = value; }
		}

		/// <summary>
		/// The type of event to be used for selecting a tab.
		/// This property maps directly to the "event" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <exception cref="ArgumentException">The enum value must be defined.</exception>
		[DefaultValue(defaultEvent), Category("Behavior")]
		[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Event", Justification = "Exposed as the jQuery option name.")]
		public virtual PanelActivateEvent Event {
			get { return ViewState["Event"] as PanelActivateEvent? ?? defaultEvent; }
			set {
				if(!Enum.IsDefined(typeof(PanelActivateEvent), value)) throw new ArgumentException("Value not defined.", "value");
				ViewState["Event"] = value;
			}
		}

		/// <summary>
		/// If the remote tab's anchor element has no title attribute to generate an id from, an id/fragment identifier is created from this prefix and a unique id returned by $.data(el), for example "ui-tabs-54".
		/// This property maps directly to the "idPrefix" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultIdPrefix), Category("Behavior")]
		public virtual string IdPrefix {
			get { return ViewState["IdPrefix"] as string ?? defaultIdPrefix; }
			set { ViewState["IdPrefix"] = value; }
		}

		//TODO: Use this to actually generate the Tab markup if possible
		/// <summary>
		/// HTML template from which a new tab panel is created in case of adding a tab with the add method or when creating a panel for a remote tab on the fly.
		/// This property maps directly to the "panelTemplate" option when the jQuery UI widget is initialized.
		/// </summary>
		[TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty), Browsable(false)]
		public virtual ITemplate PanelTemplate {
			get { return panelTemplate; }
			set { panelTemplate = value; }
		}

		/// <summary>
		/// Gets a control object to which you can programmatically add child controls for <see cref="PanelTemplate"/>.
		/// </summary>
		[Browsable(false)]
		public Control PanelTemplateContainer {
			get {
				EnsureChildControls();
				return panelTemplateContainer;
			}
		}

		//TODO: This is simply too fragile.  Lets call it ActivePanel and change it to use a ViewState control name reference, and set field with clientId, init with visible index and postback with clientId
		/// <summary>
		/// Zero-based index of the tab to be selected on initialization.
		/// This property maps directly to the "selected" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultSelected), Category("Appearance")]
		public virtual int? Selected {
			get { return ViewState["Selected"] as int? ?? defaultSelected; }
			set { ViewState["Selected"] = value; }
		}

		/// <summary>
		/// The HTML content of this string is shown in a tab title while remote content is loading. Pass in empty string to deactivate that behavior.
		/// This property maps directly to the "spinner" option when the jQuery UI widget is initialized.
		/// </summary>
		[TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty), Browsable(false)]
		public virtual ITemplate Spinner {
			get { return spinner; }
			set { spinner = value; }
		}

		/// <summary>
		/// Gets a control object to which you can programmatically add child controls for <see cref="Spinner"/>.
		/// </summary>
		[Browsable(false)]
		public Control SpinnerContainer {
			get {
				EnsureChildControls();
				return spinnerContainer;
			}
		}

		//TODO: See the feasibility of using this property as well for actual markup
		/// <summary>
		/// HTML template from which a new tab is created and added. The placeholders #{href} and #{label} are replaced with the url and tab label that are passed as arguments to the add method.
		/// This property maps directly to the "tabTemplate" option when the jQuery UI widget is initialized.
		/// </summary>
		[TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty), Browsable(false)]
		public virtual ITemplate TabTemplate {
			get { return tabTemplate; }
			set { tabTemplate = value; }
		}

		/// <summary>
		/// Gets a control object to which you can programmatically add child controls for <see cref="TabTemplate"/>.
		/// </summary>
		[Browsable(false)]
		public Control TabTemplateContainer {
			get {
				EnsureChildControls();
				return tabTemplateContainer;
			}
		}

		#endregion


		#region Properties: Widget methods

		public virtual bool SelectPending {
			get { return selectPending; }
		}

		public virtual TabPanel PendingSelect {
			get { return pendingSelect; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets the child <see cref="TabPanel"/> controls to be rendered within the <see cref="Tabs"/> widget.
		/// </summary>
		[DefaultValue(null), MergableProperty(false), PersistenceMode(PersistenceMode.InnerProperty), Category("Misc")]
		public virtual TabPanelCollection Panels {
			get {
				EnsureChildControls();
				return panelCollection;
			}
		}

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to round-trip the <see cref="Selected"/> property.
		/// </summary>
		public virtual string SelectedClientId {
			get { return string.Format(CultureInfo.InvariantCulture, "__{0}_Selected", ClientID); }
		}

		#endregion


		#region Events

		/// <summary>
		/// Occurs on postback when the <see cref="Selected"/> property value has changed since previous render.
		/// </summary>
		[Category("Action")]
		public event EventHandler SelectedChanged;

		#endregion


		#region Methods: Event hooks

		/// <summary>
		/// Raises the <see cref="SelectedChanged"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected virtual void OnSelectedChanged(EventArgs e) {
			if(SelectedChanged != null) SelectedChanged.Invoke(this, e);
		}

		#endregion


		#region Methods: Widget methods

		public void Select(int index) {
			TabPanelCollection panels = Panels;
			if(index < -1 || index >= panels.Count) throw new ArgumentOutOfRangeException("index", index, "Value must greater or equal to -1 and less than the value returned by Panels.Count.");

			if(index == -1) Select(null);
			else Select(panels[index]);
		}

		public virtual void Select(TabPanel panel) {
			if(selectPendingByPostBack && pendingSelect == panel) return;
			pendingSelect = panel;
			selectPending = true;
			selectPendingByPostBack = false;
		}

		public virtual void CancelSelect() {
			pendingSelect = null;
			selectPending = false;
			selectPendingByPostBack = false;
		}

		#endregion


		#region Methods: Child controls

		/// <summary>
		/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
		/// </summary>
		protected override void CreateChildControls() {
			base.CreateChildControls();

			if(panelCollection == null) {
				GroupControlContainer panelContainer = CreatePanelContainer();
				GroupedControls.AddContainer(panelContainer);
				panelCollection = CreatePanels(panelContainer);
			}

			if(panelTemplateContainer == null) {
				panelTemplateContainer = CreatePanelTemplateContainer();
				panelTemplateContainer.Visible = false;
				GroupedControls.AddContainer(panelTemplateContainer);
			}

			if(spinnerContainer == null) {
				spinnerContainer = CreateSpinnerContainer();
				spinnerContainer.Visible = false;
				GroupedControls.AddContainer(spinnerContainer);
			}

			if(tabTemplateContainer == null) {
				tabTemplateContainer = CreateTabTemplateContainer();
				tabTemplateContainer.Visible = false;
				GroupedControls.AddContainer(tabTemplateContainer);
			}

		}

		/// <summary>
		/// Creates a <see cref="GroupControlContainer"/> object to contain the grouped child controls for the <see cref="Panels"/> property.
		/// </summary>
		/// <returns>The <see cref="GroupControlContainer"/> to group the child controls.</returns>
		protected virtual GroupControlContainer CreatePanelContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates a <see cref="GroupControlContainer"/> object to contain the grouped child controls for the <see cref="PanelTemplate"/> property.
		/// </summary>
		/// <returns>The <see cref="GroupControlContainer"/> to group the child controls.</returns>
		protected virtual GroupControlContainer CreatePanelTemplateContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates a <see cref="GroupControlContainer"/> object to contain the grouped child controls for the <see cref="Spinner"/> property.
		/// </summary>
		/// <returns>The <see cref="GroupControlContainer"/> to group the child controls.</returns>
		protected virtual GroupControlContainer CreateSpinnerContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates a <see cref="GroupControlContainer"/> object to contain the grouped child controls for the <see cref="TabTemplate"/> property.
		/// </summary>
		/// <returns></returns>
		protected virtual GroupControlContainer CreateTabTemplateContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates a <see cref="TabPanelCollection"/> object to be used as the <see cref="Panels"/> property.
		/// </summary>
		/// <returns>A new <see cref="TabPanelCollection"/> instance.</returns>
		protected virtual TabPanelCollection CreatePanels(GroupControlContainer container) {
			return new TabPanelCollection(container);
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Raises the <see cref="Control.Init"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected override void OnInit(EventArgs e) {
			base.OnInit(e);

			ITemplate panelTemplate = PanelTemplate;
			Control panelTemplateContainer = PanelTemplateContainer;
			if(panelTemplate == null)
				panelTemplateContainer.Controls.Add(new LiteralControl(defaultPanelTemplate));
			else
				panelTemplate.InstantiateIn(panelTemplateContainer);

			ITemplate spinner = Spinner;
			Control spinnerContainer = SpinnerContainer;
			if(spinner == null)
				spinnerContainer.Controls.Add(new LiteralControl(defaultSpinner));
			else
				spinner.InstantiateIn(spinnerContainer);

			ITemplate tabTemplate = TabTemplate;
			Control tabTemplateContainer = TabTemplateContainer;
			if(tabTemplate == null)
				tabTemplateContainer.Controls.Add(new LiteralControl(defaultTabTemplate));
			else
				tabTemplate.InstantiateIn(tabTemplateContainer);

		}

		/// <summary>
		/// Raises the <see cref="Control.PreRender"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object containing the event data.</param>
		protected override void OnPreRender(EventArgs e) {
			base.OnPreRender(e);

			if(Visible) Page.RegisterRequiresPostBack(this);
		}

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The <see cref="Manager"/> used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {

			manager.RegisterClientScript(ClientScript.Core);
			manager.RegisterClientScript(ClientScript.UIWidgetFactory);
			manager.RegisterClientScript(ClientScript.UITabsWidget);

			manager.RegisterClientStyle(ClientStyle.UICore);
			manager.RegisterClientStyle(ClientStyle.UITabsWidget);
			manager.RegisterTheme();

			manager.RegisterForReadyScript(this);
		}

		/// <summary>
		/// Sends server control content to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered to the client.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void Render(HtmlTextWriter writer) {

			base.Render(writer);

			ClientScriptManager clientScript = Page.ClientScript;
			clientScript.RegisterForEventValidation(new PostBackOptions(this));

			int? selected = Selected;
			clientScript.RegisterHiddenField(SelectedClientId, selected.HasValue ? selected.Value.ToString(CultureInfo.InvariantCulture) : string.Empty);
		}

		/// <summary>
		/// Adds HTML attributes and styles that need to be rendered to the specified <see cref="HtmlTextWriter"/>.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void AddAttributesToRender(HtmlTextWriter writer) {

			//TODO: Let's revisit the whole "degradability versus getting rid of flicker issue"  I don't think this falls under UseCssStyling, perhaps a DegradeForNonJs... Or is that a moot point here?
			if(!MasterUseCssStyling)
				writer.AddStyleAttribute("display", "none");

			base.AddAttributesToRender(writer);
		}

		/// <summary>
		/// Outputs the content of a server control's children to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered on the client.
		/// This property is used primarily by control developers.
		/// </summary>
		/// <param name="writer">The System.Web.UI.HtmlTextWriter object that receives the rendered content.</param>
		protected override void RenderChildren(HtmlTextWriter writer) {

			writer.Indent++;

			bool first = true;
			if(Controls.Count != 0) {
				foreach(TabPanel panel in Panels) {
					if(!panel.Visible) continue;

					if(first) {
						first = false;
						writer.RenderBeginTag(HtmlTextWriterTag.Ul);
					} else
						writer.WriteLine();

					//<li>
					writer.RenderBeginTag(HtmlTextWriterTag.Li);

					//<a>
					string ajaxContentUrl = panel == null ? null : panel.AjaxContentUrl;
					writer.AddAttribute(HtmlTextWriterAttribute.Href, string.IsNullOrEmpty(ajaxContentUrl) ? "#" + panel.ClientID : ResolveUrl(ajaxContentUrl));
					writer.RenderBeginTag(HtmlTextWriterTag.A);
					if(panel != null) writer.Write(panel.Tab);
					writer.RenderEndTag();
					//</a>

					writer.RenderEndTag();
					//</li>
				}
				if(!first) writer.RenderEndTag();
			}

			base.RenderChildren(writer);
		}

		/// <summary>
		/// Renders the jQuery "Ready Event" script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderReadyScript(ScriptBuilder script) {
			RenderInitScript(script);
			if(!IsEnabled) RenderDisableScript(script);
			if(selectPending) RenderSelectMethodScript(script);

			//TODO: Let's revisit the whole "degradability versus getting rid of flicker issue"  I don't think this falls under UseCssStyling, perhaps a DegradeForNonJs... Or is that a moot point here?
			if(!MasterUseCssStyling) {
				script.NewLine();
				script.RenderIdSelector(ClientID);
				script.Append(".css('display', '');");
			}

		}

		/// <summary>
		/// Renders the jQuery UI initialization script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderInitScript(ScriptBuilder script) {

			script.RenderIdSelector(ClientID);
			script.Append(".tabs(");

			PropertyCollection properties = script.CreateProperties();
			AddInitProperties(properties);
			script.RenderProperties(properties);

			script.Append(");");
		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="properties">A <see cref="PropertyCollection"/> object that will become the jQuery UI initialization options.</param>
		protected virtual void AddInitProperties(PropertyCollection properties) {

			bool cache = Cache;
			if(cache != defaultCache) properties.AddValue("cache", cache);

			bool collapsible = Collapsible;
			if(collapsible != defaultCollapsible) properties.AddValue("collapsible", collapsible);

			PanelActivateEvent activateEvent = Event;
			if(activateEvent != defaultEvent) properties.AddEnumValue("event", activateEvent);

			string idPrefix = IdPrefix;
			if(idPrefix != defaultIdPrefix) properties.AddValue("idPrefix", idPrefix);

			string panelTemplate = panelTemplateContainer.RenderHtml();
			if(panelTemplate != defaultPanelTemplate) properties.AddValue("panelTemplate", panelTemplate);

			int? selected = Selected;
			if(selected != defaultSelected) {
				if(selected.HasValue) properties.AddValue("selected", selected.Value);
				else properties.AddValue("selected", false);
			}

			ScriptBuilder selectScript = properties.CreateChildScript();
			RenderSelectEventScript(selectScript);
			if(selectScript.Length != 0) properties.AddScript("select", selectScript);

			string spinner = spinnerContainer.RenderHtml();
			if(spinner != defaultSpinner) properties.AddValue("spinner", spinner);

			string tabTemplate = tabTemplateContainer.RenderHtml();
			if(tabTemplate != defaultTabTemplate) properties.AddValue("tabTemplate", tabTemplate);

		}

		/// <summary>
		/// Renders the client "change" event script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderSelectEventScript(ScriptBuilder script) {

			script.Append("function(event, ui) {");

			script.Indent++;

			script.NewLine();
			script.RenderIdSelector(SelectedClientId);
			script.Append(".val(ui.index);");

			script.Indent--;
			script.NewLine();

			script.Append("}");

		}

		protected virtual void RenderDisableScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".tabs(");
			script.AppendValue("disable");
			script.Append(");");
		}

		protected virtual void RenderSelectMethodScript(ScriptBuilder script) {

			int? selectedVisibleIndex = GetSelectedVisibleIndex();
			int? selectVisibleIndex = GetVisibleIndex(PendingSelect);

			if(selectedVisibleIndex == selectVisibleIndex) return;
			if(selectVisibleIndex == null && !Collapsible) return;

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".tabs(");
			script.AppendValue("select");
			script.Append(", ");
			if(selectVisibleIndex.HasValue) script.AppendValue(selectVisibleIndex.Value);
			else script.AppendValue(false);
			script.Append(");");

		}

		#endregion


		#region Methods: Private routines

		private int? GetSelectedVisibleIndex() {

			TabPanelCollection panels = Panels;
			int? selectedIndex = Selected;
			if(selectedIndex == null) return null;

			int visibleIndex = 0;
			for(int index = 0; index < panels.Count; index++) {
				bool visible = panels[index].Visible;

				if(index == selectedIndex) {
					if(visible) return visibleIndex;
					return null;
				}

				if(visible) visibleIndex++;
			}

			return null;
		}

		private int? GetVisibleIndex(TabPanel panel) {

			if(panel == null || !panel.Visible) return null;

			int index = 0;
			foreach(TabPanel comparePanel in Panels) {
				if(comparePanel.Visible) {
					if(panel == comparePanel) return index;
					index++;
				}
			}
			return null;
		}

		#endregion


		#region Methods: Post data

		/// <summary>
		/// Processes postback data for an ASP.NET server control.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="postDataKey">The key identifier for the control.</param>
		/// <param name="postCollection">The collection of all incoming name values.</param>
		/// <returns>True if the server control's state changes as a result of the postback; otherwise, false.</returns>
		protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

			Page.ClientScript.ValidateEvent(postDataKey);

			int? newSelected;
			if(Utility.TryParse(postCollection[SelectedClientId], out newSelected) && newSelected != Selected) {
				Selected = newSelected;
				return true;
			}

			return false;
		}

		/// <summary>
		/// Signals the server control to notify the ASP.NET application that the state of the control has changed.
		/// This method is used primarily by control developers.
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This method is amazing.  You get a complaint if you don't include the protected one, but when you do, it tells you to use events.  I included it simply because it gives the derrived control a chance to hook deeper.")]
		protected virtual void RaisePostDataChangedEvent() {
			OnSelectedChanged(EventArgs.Empty);
		}

		#endregion


		#region Explicit Implementation: IPostBackDataHandler

		bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) {
			return LoadPostData(postDataKey, postCollection);
		}

		void IPostBackDataHandler.RaisePostDataChangedEvent() {
			RaisePostDataChangedEvent();
		}

		#endregion


	}

}